#!/usr/bin/env python3
# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0

'''
A script to compare an ISS and RTL run to make sure nothing has diverged.
'''

import argparse
import os
import sys
from typing import Dict, Optional, TextIO, Tuple, Union

from scripts.scripts_lib import read_test_dot_seed
from scripts.test_entry import TestEntry, get_test_entry
from scripts.test_run_result import TestRunResult

_CORE_IBEX = os.path.normpath(os.path.join(os.path.dirname(__file__)))
_IBEX_ROOT = os.path.normpath(os.path.join(_CORE_IBEX, '../../..'))
_RISCV_DV_ROOT = os.path.join(_IBEX_ROOT, 'vendor/google_riscv-dv')
_OLD_SYS_PATH = sys.path

# Import riscv_trace_csv and lib from _DV_SCRIPTS before putting sys.path back
# as it started.
try:
    sys.path = ([os.path.join(_CORE_IBEX, 'riscv_dv_extension'),
                 os.path.join(_RISCV_DV_ROOT, 'scripts')] +
                sys.path)

    from spike_log_to_trace_csv import process_spike_sim_log  # type: ignore
    from ovpsim_log_to_trace_csv import process_ovpsim_sim_log  # type: ignore
    from instr_trace_compare import compare_trace_csv  # type: ignore

    from ibex_log_to_trace_csv import (process_ibex_sim_log,  # type: ignore
                                       check_ibex_uvm_log)
finally:
    sys.path = _OLD_SYS_PATH


_CompareResult = Tuple[bool, Optional[str], Dict[str, str]]


def compare_test_run(test: TestEntry,
                     seed: int,
                     iss: str,
                     rtl_log: str,
                     rtl_trace: str,
                     iss_trace: str,
                     en_cosim: bool,
                     cosim_trace: str,
                     binary: str,
                     compare_log: str) -> TestRunResult:
    '''Compare results for a single run of a single test

    Here, test is a dictionary describing the test (read from the testlist YAML
    file). seed is the seed that was run. iss is the chosen instruction set
    simulator (currently supported: spike and ovpsim).

    rtl_log is the log file generated by the RTL simulation. rtl_trace and
    iss_trace are the traces of instructions executed generated by the RTL
    simulation and ISS, respectively. This function generates CSV files at
    rtl_trace + '.csv' and iss_trace + '.csv'.

    binary is the path to an ELF file with the code that was executed.

    compare_log is the path where we should write a log describing the
    comparison operation.

    Returns a _CompareResult with a pass/fail flag, together with some
    information about the run (to be written to the log file).

    '''
    test_name = test['test']
    assert isinstance(test_name, str)

    kv_data = {
        'name': test_name,
        'seed': seed,
        'binary': binary,
        'uvm_log': rtl_log,
        'rtl_trace': rtl_trace,
        'rtl_trace_csv': rtl_trace + '.csv',
        'iss_trace': None,
        'iss_trace_csv': None,
        'en_cosim': en_cosim,
        'cosim_trace': None,
        'cosim_trace_csv': None,
        'comparison_log': None,
        'passed': False,
        'failure_message': None
    }

    # Have a look at the UVM log.
    # Report a failure if an issue is seen in the log.
    try:
        uvm_pass, uvm_log_lines = check_ibex_uvm_log(rtl_log)
    except IOError as e:
        kv_data['failure_message'] = str(e)
        kv_data['failure_message'] += \
            '\n[FAILED] Could not open simulation log'
        return TestRunResult(**kv_data)

    if not uvm_pass:
        kv_data['failure_message'] = '\n'.join(uvm_log_lines)
        kv_data['failure_message'] += '\n[FAILED]: sim error seen'
        return TestRunResult(**kv_data)

    # Both the cosim and non-cosim flows produce a trace from the ibex_tracer,
    # so process that file for errors.
    try:
        # Convert the RTL log file to a trace CSV.
        process_ibex_sim_log(kv_data['rtl_trace'],
                             kv_data['rtl_trace_csv'])
    except (OSError, RuntimeError) as e:
        kv_data['failure_message'] = \
            '[FAILED]: Log processing failed: {}'.format(e)
        return TestRunResult(**kv_data)

    if en_cosim:
        # Process the cosim logfile to check for errors
        kv_data['cosim_trace'] = cosim_trace
        kv_data['cosim_trace_csv'] = cosim_trace + '.csv'
        try:
            if iss == "spike":
                process_spike_sim_log(kv_data['cosim_trace'],
                                      kv_data['cosim_trace_csv'])
            else:
                raise RuntimeError('Unsupported simulator for cosim')
        except (OSError, RuntimeError) as e:
            kv_data['failure_message'] = \
                '[FAILED]: Log processing failed: {}'.format(e)
            return TestRunResult(**kv_data)

        # The comparison has already passed, since we passed the simulation
        kv_data['passed'] = True
        return TestRunResult(**kv_data)

    else:
        # no_post_compare skips the final ISS v RTL log check, so if we've reached
        # here we're done when no_post_compare is set.
        no_post_compare = test.get('no_post_compare', False)
        assert isinstance(no_post_compare, bool)
        if no_post_compare:
            kv_data['passed'] = True
            return TestRunResult(**kv_data)

        # There were no UVM errors. Process the log file from the ISS. Note that
        # the filename is a bit odd-looking. This is silly, but it ensures that
        # riscv-dv's cov.py script won't pick it up for architectural coverage.
        kv_data['iss_trace'] = iss_trace
        kv_data['iss_trace_csv'] = iss_trace + '-csv'
        try:
            if iss == "spike":
                process_spike_sim_log(kv_data['iss_trace'],
                                      kv_data['iss_trace_csv'])
            else:
                assert iss == 'ovpsim'  # (should be checked by argparse)
                process_ovpsim_sim_log(kv_data['iss_trace'],
                                       kv_data['iss_trace_csv'])
        except (OSError, RuntimeError) as e:
            kv_data['failure_message'] = \
                '[FAILED]: Log processing failed: {}'.format(e)
            return TestRunResult(**kv_data)

        kv_data['comparison_log'] = compare_log
        # Delete any existing file at compare_log
        # (the compare_trace_csv function would append to it, which is rather
        # confusing).
        try:
            os.remove(compare_log)
        except FileNotFoundError:
            pass

        compare_result = \
            compare_trace_csv(kv_data['rtl_trace_csv'],
                              kv_data['iss_trace_csv'],
                              "ibex", iss, compare_log,
                              **test.get('compare_opts', {}))

        try:
            compare_log_file = open(compare_log)
            compare_log_contents = compare_log_file.read()
            compare_log_file.close()
        except IOError as e:
            kv_data['failure_message'] = \
                '[FAILED]: Could not read compare log: {}'.format(e)
            return TestRunResult(**kv_data)

        # Rather oddly, compare_result is a string. The comparison passed if it
        # starts with '[PASSED]: ' and failed otherwise.
        compare_passed = compare_result.startswith('[PASSED]: ')
        kv_data['passed'] = compare_passed
        if not compare_passed:
            assert compare_result.startswith('[FAILED]: ')
            kv_data['failure_message'] = ('RTL / ISS trace comparison failed\n' +
                                          compare_log_contents)
            return TestRunResult(**kv_data)

        return TestRunResult(**kv_data)


# If any of these characters are present in a string output it in multi-line
# mode. This will either be because the string contains newlines or other
# characters that would otherwise need escaping
_YAML_MULTILINE_CHARS = ['[', ']', ':', "'", '"', '\n']


def yaml_format(val: Union[int, str, bool]) -> str:
    '''Format a value for yaml output.

    For int, str and bool value can just be converted to str with special
    handling for some string
    '''

    # If val is a multi-line string
    if isinstance(val, str) and any(c in val for c in _YAML_MULTILINE_CHARS):
        # Split into individual lines and output them after a suitable yaml
        # multi-line string indicator ('|-') indenting each line.
        lines = val.split('\n')
        return '|-\n' + '\n'.join([f'  {line}' for line in lines])

    if val is None:
        return ''

    return str(val)


def on_result(result: TestRunResult, output: TextIO) -> None:
    kv_data = result._asdict()

    klen = 1
    for k in kv_data:
        klen = max(klen, len(k))

    for k, v in kv_data.items():
        kpad = ' ' * (klen - len(k))
        output.write(f'{k}:{kpad} {yaml_format(v)}\n')


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument('--test-dot-seed',
                        type=read_test_dot_seed,
                        required=True)
    parser.add_argument('--iss', required=True, choices=['spike', 'ovpsim'])
    parser.add_argument('--iss-trace', required=True)
    parser.add_argument('--rtl-log', required=True)
    parser.add_argument('--rtl-trace', required=True)
    parser.add_argument('--en_cosim', required=False, action='store_true')
    parser.add_argument('--cosim-trace', required=True)
    parser.add_argument('--binary', required=True)
    parser.add_argument('--compare-log', required=True)
    parser.add_argument('--output', required=True)

    args = parser.parse_args()

    testname, seed = args.test_dot_seed

    entry = get_test_entry(testname)

    result = compare_test_run(entry, seed, args.iss,
                              args.rtl_log,
                              args.rtl_trace, args.iss_trace,
                              args.en_cosim, args.cosim_trace,
                              args.binary, args.compare_log)

    with open(args.output, 'w', encoding='UTF-8') as outfile:
        on_result(result, outfile)

    # Always return 0 (success), even if the test failed. We've successfully
    # generated a comparison log either way and we don't want to stop Make from
    # gathering them all up for us.
    return 0


if __name__ == '__main__':
    sys.exit(main())
